/*
 * Copyright (c) 2011 Bo Zhou<Bo.Schwarzstein@gmail.com>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 
		http://www.apache.org/licenses/LICENSE-2.0 
 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// An implementation of "Reflection Model of Metallic Paints for Reflectance Acquisition".

#include <cstdio>
#include <cmath>
#include <algorithm>
#include <shader.h>

static inline double erf(double x)
{
	double y = 1.0 / (1.0 + 0.3275911 * x);
	return 1.0 - (((((1.061405429 * y - 1.453152027) * y + 1.421413741) * y - 0.284496736) * y + 0.254829592) * y) * exp(- x * x);
}

typedef struct {
	miScalar	kD;
	miColor		colorD;
	miScalar	exponentD;
	miScalar	kS;
	miColor		colorS;
	miScalar	exponentS;
	miScalar	kF;
	miColor		colorF;
	miScalar	exponentF;
	miInteger	samplesF;
	miScalar	sigmaF;
	miScalar	muF;
	miScalar	kFG;
	miInteger	mode;
	int		i_lights;
	int		n_lights;
	miTag		lights[1];
} metallicPaint_t;



miC_LINKAGE DLLEXPORT
int metallicPaint_version(void)
{
	return 1;
}


miC_LINKAGE DLLEXPORT
miBoolean metallicPaint(
	miColor *result,
	miState *state,
	metallicPaint_t *param)
{
	if (state->type == miRAY_SHADOW || state->type == miRAY_DISPLACE)
	{
		return miFALSE;
	}

	miScalar kD = *mi_eval_scalar(&param->kD);
	miColor colorD = *mi_eval_color(&param->colorD);
	miScalar exponentD = *mi_eval_scalar(&param->exponentD);

	miScalar kS = *mi_eval_scalar(&param->kS);
	miColor colorS = *mi_eval_color(&param->colorS);
	miScalar exponentS = *mi_eval_scalar(&param->exponentS);

	miScalar kF = *mi_eval_scalar(&param->kF);
	miColor colorF = *mi_eval_color(&param->colorF);
	miScalar exponentF = *mi_eval_scalar(&param->exponentF);
	miInteger samplesF = *mi_eval_integer(&param->samplesF);
	miScalar sigmaF = *mi_eval_scalar(&param->sigmaF);
	miScalar muF = *mi_eval_scalar(&param->muF);

	miScalar kFG = *mi_eval_scalar(&param->kFG);

	miInteger mode = *mi_eval_integer(&param->mode);
	miInteger i_lights = *mi_eval_integer(&param->i_lights);
	miInteger n_lights = *mi_eval_integer(&param->n_lights);
	miTag* lights =  mi_eval_tag(param->lights) + i_lights;

	if (mode == 1)
	{
		mi_inclusive_lightlist(&n_lights, &lights, state);
	}
	else if (mode == 2)
	{
		mi_exclusive_lightlist(&n_lights, &lights, state);
	}
	else if (mode == 4)
	{
		n_lights = 0;
		lights = NULL;
	}
	
	if (mode == 4 || n_lights)
	{
		miVector V = state->dir;
		mi_vector_neg(&V);
		mi_vector_normalize(&V);

		miVector N = state->normal;
		mi_vector_normalize(&N);

		for (mi::shader::LightIterator itr(state, lights, n_lights); !itr.at_end(); ++itr)
		{
			miColor resultD;
			resultD.r = resultD.g = resultD.b = resultD.a = 0;
			miColor resultS;
			resultS.r = resultS.g = resultS.b = resultS.a = 0;
			miColor resultF;
			resultF.r = resultF.g = resultF.b = resultF.a = 0;
			while (itr->sample())
			{
				miColor Cl;
				itr->get_contribution(&Cl);

				miVector L = itr->get_direction();
				mi_vector_normalize(&L);

				miVector H;
				mi_vector_add(&H, &L, &V);
				mi_vector_normalize(&H);

				// Diffuse
				//
				if (kD > 1e-6f)
				{
					miScalar dotNL = itr->get_dot_nl();
					miScalar attenuDotNL = powf(dotNL, exponentD);

					resultD.r += colorD.r * attenuDotNL * Cl.r;
					resultD.g += colorD.g * attenuDotNL * Cl.g;
					resultD.b += colorD.b * attenuDotNL * Cl.b;
				}

				// Specular
				//
				if (kS > 1e-6f)
				{
					miScalar dotNH = mi_vector_dot(&N, &H);
					miScalar attenuDotNH = powf(dotNH, exponentS);

					resultS.r += colorS.r * attenuDotNH * Cl.r;
					resultS.g += colorS.g * attenuDotNH * Cl.g;
					resultS.b += colorS.b * attenuDotNH * Cl.b;
				}

				// Flake
				//
				if (kF > 1e-6f)
				{
					miScalar dotNH = mi_vector_dot(&N, &H);
					miScalar sinNH = sqrtf(1.0f - dotNH * dotNH);

					double sample = 0.0;
					int sampleNumber = 0;
					while (mi_sample(&sample, &sampleNumber, state, 1, (miUint*)&samplesF))
					{
						static const miScalar sqrtTwo = sqrtf(2.0f);
						miScalar offset = muF + sigmaF * sqrtTwo / (miScalar)erf(2.0 * sample - 1.0);
						miScalar cosOffset = std::min(cosf(offset), 1.0f);
						miScalar sinOffset = sqrtf(1.0f - cosOffset * cosOffset);
						miScalar attenuDotNHWithOffset = powf(dotNH * cosOffset - sinNH * sinOffset, exponentF);
						resultF.a += attenuDotNHWithOffset;
					}
					miScalar inverseSamplesF = 1.0f / samplesF;
					resultF.r += colorF.r * resultF.a * Cl.r * inverseSamplesF; 
					resultF.g += colorF.g * resultF.a * Cl.g * inverseSamplesF;
					resultF.b += colorF.b * resultF.a * Cl.b * inverseSamplesF;
				}
			}
			miInteger samples = itr->get_number_of_samples();
			if (samples > 1)
			{
				miScalar inverseSamples = 1.0f / samples;

				resultD.r *= inverseSamples;
				resultD.g *= inverseSamples;
				resultD.b *= inverseSamples;

				resultS.r *= inverseSamples;
				resultS.g *= inverseSamples;
				resultS.b *= inverseSamples;

				resultF.r *= inverseSamples;
				resultF.g *= inverseSamples;
				resultF.b *= inverseSamples;
			}

			result->r += kD * resultD.r;
			result->g += kD * resultD.g;
			result->b += kD * resultD.b;

			result->r += kS * resultS.r;
			result->g += kS * resultS.g;
			result->b += kS * resultS.b;

			result->r += kF * resultF.r;
			result->g += kF * resultF.g;
			result->b += kF * resultF.b;
		}



		if (kFG > 1e-6f)
		{
			miColor finalGatherResult;
			if (mi_compute_avg_radiance(&finalGatherResult, state, 'f', NULL))
			{
				result->r += kFG * colorD.r * finalGatherResult.r;
				result->g += kFG * colorD.g * finalGatherResult.g;
				result->b += kFG * colorD.b * finalGatherResult.b;
			}
			
		}
	}

	result->a = 1.0f;

	return miTRUE;
}
